package org.andengine.util.adt.spatial.quadtree;

import org.andengine.util.IMatcher;
import org.andengine.util.adt.bounds.BoundsSplit;
import org.andengine.util.adt.bounds.BoundsSplit.BoundsSplitException;
import org.andengine.util.adt.bounds.IIntBounds;
import org.andengine.util.adt.bounds.IntBounds;
import org.andengine.util.adt.spatial.ISpatialItem;
import org.andengine.util.adt.spatial.bounds.util.IntBoundsUtils;

import java.util.ArrayList;
import java.util.List;


/**
 * (c) Zynga 2011
 *
 * @author Nicolas Gramlich <ngramlich@zynga.com>
 * @since 20:22:18 - 10.10.2011
 */
public class IntQuadTree<T extends ISpatialItem<IIntBounds>> extends QuadTree<IIntBounds, T> implements IIntBounds {
    // ===========================================================
    // Constants
    // ===========================================================

    // ===========================================================
    // Fields
    // ===========================================================

    private final IntBounds mQueryIntBounds = new IntBounds(0, 0, 0, 0);

    // ===========================================================
    // Constructors
    // ===========================================================

    public IntQuadTree(final IIntBounds pIntBounds) {
        super(pIntBounds);
    }

    public IntQuadTree(final int pXMin, final int pYMin, final int pXMax, final int pYMax) {
        super(new IntBounds(pXMin, pYMin, pXMax, pYMax));
    }

    public IntQuadTree(final IIntBounds pIntBounds, final int pMaxLevel) {
        super(pIntBounds, pMaxLevel);
    }

    public IntQuadTree(final int pXMin, final int pYMin, final int pXMax, final int pYMax, final int pMaxLevel) {
        super(new IntBounds(pXMin, pYMin, pXMax, pYMax), pMaxLevel);
    }

    @Override
    protected IntQuadTreeNode initRoot(final IIntBounds pIntBounds) {
        return new IntQuadTreeNode(QuadTree.LEVEL_ROOT, pIntBounds);
    }

    // ===========================================================
    // Getter & Setter
    // ===========================================================

    @Override
    public int getXMin() {
        return this.getRoot().getXMin();
    }

    @Override
    public int getYMin() {
        return this.getRoot().getYMin();
    }

    @Override
    public int getXMax() {
        return this.getRoot().getXMax();
    }

    @Override
    public int getYMax() {
        return this.getRoot().getYMax();
    }

    // ===========================================================
    // Methods for/from SuperClass/Interfaces
    // ===========================================================

    @SuppressWarnings("unchecked")
    @Override
    protected IntQuadTreeNode getRoot() {
        return (IntQuadTreeNode) this.mRoot;
    }

    // ===========================================================
    // Methods
    // ===========================================================

    public synchronized ArrayList<T> query(final int pX, final int pY) {
        this.mQueryIntBounds.set(pX, pY);
        return this.query(this.mQueryIntBounds);
    }

    public synchronized <L extends List<T>> L query(final int pX, final int pY, final L pResult) {
        this.mQueryIntBounds.set(pX, pY);
        return this.query(this.mQueryIntBounds, pResult);
    }

    public synchronized ArrayList<T> query(final int pX, final int pY, final IMatcher<T> pMatcher) {
        this.mQueryIntBounds.set(pX, pY);
        return this.query(this.mQueryIntBounds, pMatcher);
    }

    public synchronized <L extends List<T>> L query(final int pX, final int pY, final IMatcher<T> pMatcher, final L pResult) {
        this.mQueryIntBounds.set(pX, pY);
        return this.query(this.mQueryIntBounds, pMatcher, pResult);
    }

    public synchronized ArrayList<T> query(final int pXMin, final int pYMin, final int pXMax, final int pYMax) {
        this.mQueryIntBounds.set(pXMin, pYMin, pXMax, pYMax);
        return this.query(this.mQueryIntBounds);
    }

    public synchronized <L extends List<T>> L query(final int pXMin, final int pYMin, final int pXMax, final int pYMax, final L pResult) {
        this.mQueryIntBounds.set(pXMin, pYMin, pXMax, pYMax);
        return this.query(this.mQueryIntBounds, pResult);
    }

    public synchronized ArrayList<T> query(final int pXMin, final int pYMin, final int pXMax, final int pYMax, final IMatcher<T> pMatcher) {
        this.mQueryIntBounds.set(pXMin, pYMin, pXMax, pYMax);
        return this.query(this.mQueryIntBounds, pMatcher);
    }

    public synchronized <L extends List<T>> L query(final int pXMin, final int pYMin, final int pXMax, final int pYMax, final IMatcher<T> pMatcher, final L pResult) {
        this.mQueryIntBounds.set(pXMin, pYMin, pXMax, pYMax);
        return this.query(this.mQueryIntBounds, pMatcher, pResult);
    }

    public synchronized <L extends List<S>, S extends T> L queryForSubclass(final int pX, final int pY, final IMatcher<T> pMatcher, final L pResult) throws ClassCastException {
        this.mQueryIntBounds.set(pX, pY);
        return this.queryForSubclass(this.mQueryIntBounds, pMatcher, pResult);
    }

    public synchronized <L extends List<S>, S extends T> L queryForSubclass(final int pXMin, final int pYMin, final int pXMax, final int pYMax, final IMatcher<T> pMatcher, final L pResult) throws ClassCastException {
        this.mQueryIntBounds.set(pXMin, pYMin, pXMax, pYMax);
        return this.queryForSubclass(this.mQueryIntBounds, pMatcher, pResult);
    }

    public synchronized boolean containsAny(final int pX, final int pY) {
        this.mQueryIntBounds.set(pX, pY);
        return this.containsAny(this.mQueryIntBounds);
    }

    public synchronized boolean containsAny(final int pXMin, final int pYMin, final int pXMax, final int pYMax) {
        this.mQueryIntBounds.set(pXMin, pYMin, pXMax, pYMax);
        return this.containsAny(this.mQueryIntBounds);
    }

    public synchronized boolean containsAny(final int pX, final int pY, final IMatcher<T> pMatcher) {
        this.mQueryIntBounds.set(pX, pY);
        return this.containsAny(this.mQueryIntBounds, pMatcher);
    }

    public synchronized boolean containsAny(final int pXMin, final int pYMin, final int pXMax, final int pYMax, final IMatcher<T> pMatcher) {
        this.mQueryIntBounds.set(pXMin, pYMin, pXMax, pYMax);
        return this.containsAny(this.mQueryIntBounds, pMatcher);
    }

    // ===========================================================
    // Inner and Anonymous Classes
    // ===========================================================

    public class IntQuadTreeNode extends QuadTreeNode implements IIntBounds {
        // ===========================================================
        // Constants
        // ===========================================================

        // ===========================================================
        // Fields
        // ===========================================================

        private final int mXMin;
        private final int mYMin;
        private final int mXMax;
        private final int mYMax;

        // ===========================================================
        // Constructors
        // ===========================================================

        public IntQuadTreeNode(final int pLevel, final IIntBounds pIntBounds) {
            this(pLevel, pIntBounds.getXMin(), pIntBounds.getYMin(), pIntBounds.getXMax(), pIntBounds.getYMax());
        }

        public IntQuadTreeNode(final int pLevel, final int pXMin, final int pYMin, final int pXMax, final int pYMax) {
            super(pLevel);

            this.mXMin = pXMin;
            this.mYMin = pYMin;
            this.mXMax = pXMax;
            this.mYMax = pYMax;

            if (pXMin > pXMax) {
                throw new IllegalArgumentException("pXMin must be smaller or equal to pXMax.");
            }
            if (pYMin > pYMax) {
                throw new IllegalArgumentException("pYMin must be smaller or equal to pYMax.");
            }
        }

        // ===========================================================
        // Getter & Setter
        // ===========================================================

        @Override
        public int getXMin() {
            return this.mXMin;
        }

        @Override
        public int getYMin() {
            return this.mYMin;
        }

        @Override
        public int getXMax() {
            return this.mXMax;
        }

        @Override
        public int getYMax() {
            return this.mYMax;
        }

        public int getWidth() {
            return this.mXMax - this.mXMin + 1;
        }

        public int getHeight() {
            return this.mYMax - this.mYMin + 1;
        }

        // ===========================================================
        // Methods for/from SuperClass/Interfaces
        // ===========================================================

        @Override
        protected IntQuadTreeNode split(final BoundsSplit pBoundsSplit) {
            final int width = this.getWidth();
            final int height = this.getHeight();

            if (width <= 2 && height <= 2) {
                throw new BoundsSplitException();
            }

            final int xMin = this.getXMin(pBoundsSplit);
            final int yMin = this.getYMin(pBoundsSplit);
            final int xMax = this.getXMax(pBoundsSplit);
            final int yMax = this.getYMax(pBoundsSplit);

            return new IntQuadTreeNode(this.mLevel + 1, xMin, yMin, xMax, yMax);
        }

        @Override
        protected boolean contains(final IIntBounds pIntBounds) {
            return this.contains(pIntBounds.getXMin(), pIntBounds.getYMin(), pIntBounds.getXMax(), pIntBounds.getYMax());
        }

        @Override
        protected boolean contains(final BoundsSplit pBoundsSplit, final IIntBounds pIntBounds) {
            return IntBoundsUtils.contains(this.getXMin(pBoundsSplit), this.getYMin(pBoundsSplit), this.getXMax(pBoundsSplit), this.getYMax(pBoundsSplit), pIntBounds.getXMin(), pIntBounds.getYMin(), pIntBounds.getXMax(), pIntBounds.getYMax());
        }

        @Override
        protected boolean intersects(final IIntBounds pIntBounds) {
            return IntBoundsUtils.intersects(this.mXMin, this.mYMin, this.mXMax, this.mYMax, pIntBounds.getXMin(), pIntBounds.getYMin(), pIntBounds.getXMax(), pIntBounds.getYMax());
        }

        @Override
        protected boolean intersects(final IIntBounds pIntBoundsA, final IIntBounds pIntBoundsB) {
            return IntBoundsUtils.intersects(pIntBoundsA, pIntBoundsB);
        }

        @Override
        protected boolean containedBy(final IIntBounds pBounds) {
            return IntBoundsUtils.contains(pBounds.getXMin(), pBounds.getYMin(), pBounds.getXMax(), pBounds.getYMax(), this.mXMin, this.mYMin, this.mXMax, this.mYMax);
        }

        @Override
        protected void appendBoundsToString(final StringBuilder pStringBuilder) {
            pStringBuilder
                    .append("[XMin: ")
                    .append(this.mXMin)
                    .append(", YMin: ")
                    .append(this.mYMin)
                    .append(", XMax: ")
                    .append(this.mXMax)
                    .append(", YMax: ")
                    .append(this.mYMax)
                    .append("]");
        }

        // ===========================================================
        // Methods
        // ===========================================================

        private int getXMin(final BoundsSplit pBoundsSplit) {
            final int width = this.getWidth();
            final int halfWidth = width / 2;

            if (width <= 2) {
                switch (pBoundsSplit) {
                    case TOP_LEFT:
                    case BOTTOM_LEFT:
                        return this.mXMin;
                    case BOTTOM_RIGHT:
                    case TOP_RIGHT:
                        throw new BoundsSplitException();
                    default:
                        throw new IllegalArgumentException("Unexpected " + BoundsSplit.class.getSimpleName() + ": '" + pBoundsSplit + "'.");
                }
            } else {
                switch (pBoundsSplit) {
                    case TOP_LEFT:
                        return this.mXMin;
                    case TOP_RIGHT:
                        return this.mXMin + halfWidth;
                    case BOTTOM_LEFT:
                        return this.mXMin;
                    case BOTTOM_RIGHT:
                        return this.mXMin + halfWidth;
                    default:
                        throw new IllegalArgumentException("Unexpected " + BoundsSplit.class.getSimpleName() + ": '" + pBoundsSplit + "'.");
                }
            }
        }

        private int getYMin(final BoundsSplit pBoundsSplit) {
            final int height = this.getHeight();
            final int halfHeight = height / 2;

            if (height <= 2) {
                switch (pBoundsSplit) {
                    case TOP_LEFT:
                    case TOP_RIGHT:
                        return this.mYMin;
                    case BOTTOM_LEFT:
                    case BOTTOM_RIGHT:
                        throw new BoundsSplitException();
                    default:
                        throw new IllegalArgumentException("Unexpected " + BoundsSplit.class.getSimpleName() + ": '" + pBoundsSplit + "'.");
                }
            } else {
                switch (pBoundsSplit) {
                    case TOP_LEFT:
                        return this.mYMin;
                    case TOP_RIGHT:
                        return this.mYMin;
                    case BOTTOM_LEFT:
                        return this.mYMin + halfHeight;
                    case BOTTOM_RIGHT:
                        return this.mYMin + halfHeight;
                    default:
                        throw new IllegalArgumentException("Unexpected " + BoundsSplit.class.getSimpleName() + ": '" + pBoundsSplit + "'.");
                }
            }
        }

        private int getXMax(final BoundsSplit pBoundsSplit) {
            final int width = this.getWidth();
            final int halfWidth = width / 2;

            if (width <= 2) {
                switch (pBoundsSplit) {
                    case TOP_LEFT:
                    case BOTTOM_LEFT:
                        return this.mXMax;
                    case BOTTOM_RIGHT:
                    case TOP_RIGHT:
                        throw new BoundsSplitException();
                    default:
                        throw new IllegalArgumentException("Unexpected " + BoundsSplit.class.getSimpleName() + ": '" + pBoundsSplit + "'.");
                }
            } else {
                switch (pBoundsSplit) {
                    case TOP_LEFT:
                        return this.mXMin + halfWidth;
                    case TOP_RIGHT:
                        return this.mXMax;
                    case BOTTOM_LEFT:
                        return this.mXMin + halfWidth;
                    case BOTTOM_RIGHT:
                        return this.mXMax;
                    default:
                        throw new IllegalArgumentException("Unexpected " + BoundsSplit.class.getSimpleName() + ": '" + pBoundsSplit + "'.");
                }
            }
        }

        private int getYMax(final BoundsSplit pBoundsSplit) {
            final int height = this.getHeight();
            final int halfHeight = height / 2;

            if (height <= 2) {
                switch (pBoundsSplit) {
                    case TOP_LEFT:
                    case TOP_RIGHT:
                        return this.mYMax;
                    case BOTTOM_LEFT:
                    case BOTTOM_RIGHT:
                        throw new BoundsSplitException();
                    default:
                        throw new IllegalArgumentException("Unexpected " + BoundsSplit.class.getSimpleName() + ": '" + pBoundsSplit + "'.");
                }
            } else {
                switch (pBoundsSplit) {
                    case TOP_LEFT:
                        return this.mYMin + halfHeight;
                    case TOP_RIGHT:
                        return this.mYMin + halfHeight;
                    case BOTTOM_LEFT:
                        return this.mYMax;
                    case BOTTOM_RIGHT:
                        return this.mYMax;
                    default:
                        throw new IllegalArgumentException("Unexpected " + BoundsSplit.class.getSimpleName() + ": '" + pBoundsSplit + "'.");
                }
            }
        }

        public boolean intersects(final int pXMin, final int pYMin, final int pXMax, final int pYMax) {
            return IntBoundsUtils.intersects(this.mXMin, this.mYMin, this.mXMax, this.mYMax, pXMin, pYMin, pXMax, pYMax);
        }

        public boolean contains(final int pXMin, final int pYMin, final int pXMax, final int pYMax) {
            return IntBoundsUtils.contains(this.mXMin, this.mYMin, this.mXMax, this.mYMax, pXMin, pYMin, pXMax, pYMax);
        }

        // ===========================================================
        // Inner and Anonymous Classes
        // ===========================================================
    }
}
